﻿#region COPYRIGHT© 2009-2012 Phillip Clark. All rights reserved.
// For licensing information see License.txt (MIT style licensing).
#endregion

using FlitBit.Parse;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Globalization;
using System.Collections.Generic;
using System.Linq;
using System;

namespace FlitBit.Parse.Tests
{
	/// <summary>
	/// Tests the CharRule class
	/// </summary>
	[TestClass]
	public class CharRuleTests
	{
		public TestContext TestContext { get; set; }

		[TestMethod]
		public void MatchExact()
		{
			var a = CharMatch.Exact('a');
			var t_or_T = CharMatch.CaseInsensitive('T');
			var m = CharRule.MatchExact(a, t_or_T);
			var input = @"we're going to match an a followed by an 't' or a 'T', there's aTleasT one but not 'AT here'";
			//                            ^16                                            ^63  
			Cursor c = new Cursor(0);
			AdvanceWithNoMatch(16, m, ref c, input);
			Assert.IsTrue(m.TryAt(input, ref c));
			Assert.AreEqual(18, (int)c);

			AdvanceWithNoMatch(63, m, ref c, input);
			Assert.IsTrue(m.TryAt(input, ref c));
			Assert.AreEqual(65, (int)c);


			AdvanceWithNoMatch(input.Length, m, ref c, input);

			Assert.AreEqual(CharRule.MatchExact(a, t_or_T), m);
		}

		[TestMethod]
		public void MatchAny()
		{
			var l = CharMatch.Exact('l');
			var t_or_T = CharMatch.CaseInsensitive('T');
			var rule = CharRule.MatchAny(l, t_or_T);
			var input = "we're going to match a bunch of els and tees', there's aTleasT one of each";
			var match = "            ^12  ^17             ^33    ^40    ^47      ^^   ^ 56, 57, 61";

			int[] positions = FindMatchingIndexes(input, rule);
			for (int i = 0; i < match.Length; i++)
			{
				if (match[i] == '^')
					Assert.IsTrue(positions.Contains(i));
				else
					Assert.IsFalse(positions.Contains(i));
			}

			Assert.AreEqual(CharRule.MatchAny(l, t_or_T), rule);
		}

		[TestMethod]
		public void MatchAll()
		{
			var letter = CharMatch.UnicodeCategory(UnicodeCategory.LowercaseLetter);
			var not_t = CharMatch.AnyExcept('t');
			var rule = CharRule.MatchAll(letter, not_t);
			var input = "matching all lower case characters except 't'";
			var match = "  ^2    ^8  ^12   ^18  ^23    ^30 ^34   ^^^^^ 40, 41, 42, 43, 44";

			int[] positions = FindNonMatchingIndexes(input, rule);
			for (int i = 0; i < match.Length; i++)
			{
				if (match[i] == '^')
					Assert.IsTrue(positions.Contains(i));
				else
					Assert.IsFalse(positions.Contains(i));
			}

			Assert.AreEqual(CharRule.MatchAll(letter, not_t), rule);
		}

		[TestMethod]
		public void MatchSingleQuotedString()
		{
			var rule = Match.MatchExact(CharRule.MatchExact('\'')
				, Match.MatchRepeatedly(Match.MatchAny(CharRule.MatchNone('\\', '\''), CharRule.MatchExact('\\', '\'')))
				, CharRule.MatchExact('\''));

			var input = "match a 'single quoted string' < just like that";
			var match = "'single quoted string'";

			Capture capture = input.CaptureFirst(rule);
			Assert.IsTrue(capture.IsValid);
			Assert.AreEqual(rule, capture.Rule);
			Assert.AreEqual(new Cursor(8), capture.Position);
			Assert.AreEqual(match.Length, capture.Length);
			Assert.AreEqual(match, capture.Value);

			input = @"match another 'single quoted string with an embedded \' or two' < just like that";
			match = @"'single quoted string with an embedded \' or two'";

			capture = input.CaptureFirst(rule);
			Assert.IsTrue(capture.IsValid);
			Assert.AreEqual(rule, capture.Rule);
			Assert.AreEqual(new Cursor(14), capture.Position);
			Assert.AreEqual(match.Length, capture.Length);
			Assert.AreEqual(match, capture.Value);

		}

		[TestMethod]
		public void MatchNewLine()
		{
			var rule = CharRule.NewLine;

			var input = "match an end of line\r\n< there's one\r< and another\u2028< and another\u2029< and another";

			Cursor cursor = new Cursor(0);
			Capture capture = input.CaptureNext(rule, ref cursor);
			Assert.IsTrue(capture.IsValid);
			Assert.AreEqual(rule, capture.Rule);
			Assert.AreEqual(new Cursor(20), capture.Position);
			Assert.AreEqual(2, capture.Length);
			Assert.AreEqual("\r\n", capture.Value);

			capture = input.CaptureNext(rule, ref cursor);
			Assert.IsTrue(capture.IsValid);
			Assert.AreEqual(rule, capture.Rule);
			Cursor pos = new Cursor(20).Newline(2).Increment(13);
			Assert.AreEqual(pos, capture.Position);
			Assert.AreEqual(1, capture.Length);
			Assert.AreEqual("\r", capture.Value);
		}

		private int[] FindNonMatchingIndexes(string input, CharRule rule)
		{
			List<int> result = new List<int>();

			Cursor cursor = new Cursor(0);
			while (cursor < input.Length)
			{
				if (!rule.TryAt(input, ref cursor))
				{
					result.Add(cursor);
					cursor++;
				}
			}

			return result.ToArray();
		}

		private int[] FindMatchingIndexes(string input, CharRule rule)
		{
			List<int> result = new List<int>();

			Cursor cursor = new Cursor(0);
			while (cursor < input.Length)
			{
				int cc = cursor;
				if (rule.TryAt(input, ref cursor))
				{
					result.Add(cc);
				}
				else
				{
					cursor++;
				}
			}

			return result.ToArray();
		}

		private void AdvanceWithMatch(int upperLimit, CharRule rule, ref Cursor c, string input)
		{
			while (c < upperLimit)
			{
				Assert.IsTrue(rule.TryAt(input, ref c));
			}
		}

		private void AdvanceWithNoMatch(int upperLimit, CharRule rule, ref Cursor c, string input)
		{
			for (; c < upperLimit; c++)
			{
				int cc = c;
				Assert.IsFalse(rule.At(input, c));
				Assert.AreEqual(cc, c.Offset, "cursor should not advance unless there is a match");
			}
		}

		[TestMethod]
		public void CaptureIsoYear()
		{
			var rule = CharRule.Iso8601Year;

			var input = "1863";

			Cursor cursor = new Cursor(0);
			Capture capture = input.CaptureNext(rule, ref cursor);
			Assert.IsTrue(capture.IsValid);
			Assert.AreEqual(rule, capture.Rule);
			Assert.AreEqual((Cursor)0, capture.Position);
			Assert.AreEqual(4, capture.Length);
			Assert.AreEqual(input, capture.Value);			
		}

		[TestMethod]
		public void CaptureIsoYearExpanded()
		{
			var rule = CharRule.Iso8601YearExpanded;

			var input = "+11863";

			Cursor cursor = new Cursor(0);
			Capture capture = input.CaptureNext(rule, ref cursor);
			Assert.IsTrue(capture.IsValid);
			Assert.AreEqual(rule, capture.Rule);
			Assert.AreEqual((Cursor)0, capture.Position);
			Assert.AreEqual(6, capture.Length);
			Assert.AreEqual(input, capture.Value);
		}

		[TestMethod]
		public void CaptureIsoMonth()
		{
			var rule = CharRule.Iso8601Month;

			var input = "0001020304050607080910111213";


			Cursor cursor = new Cursor(0);
			Capture capture = default(Capture);
			Assert.IsFalse(rule.TryCapture(input, ref cursor, out capture));
			Assert.AreEqual((Cursor)0, cursor);
			Assert.IsFalse(capture.IsValid);

			cursor += 2;
			for (int i = 1; i <= 12; i++)
			{
				int pos = cursor;			
				Assert.IsTrue(rule.TryCapture(input, ref cursor, out capture));
				Assert.IsTrue(capture.IsValid);
				Assert.AreEqual(rule, capture.Rule);
				Assert.AreEqual((Cursor)pos, capture.Position);
				Assert.AreEqual(2, capture.Length);
				Assert.AreEqual(i.ToString("00"), capture.Value);
			}

			Assert.IsFalse(rule.TryCapture(input, ref cursor, out capture));
			Assert.IsFalse(capture.IsValid);
		}

		[TestMethod]
		public void CaptureIsoDay()
		{
			var rule = CharRule.Iso8601Day;
						
			var input = @"000102030405060708091011121314151617181920212223242526272829303132";


			Cursor cursor = new Cursor(0);
			Capture capture = default(Capture);
			Assert.IsFalse(rule.TryCapture(input, ref cursor, out capture));
			Assert.AreEqual((Cursor)0, cursor);
			Assert.IsFalse(capture.IsValid, "days of the month number 1 thru 31");

			cursor += 2;
			for (int i = 1; i <= 31; i++)
			{
				int pos = cursor;
				Assert.IsTrue(rule.TryCapture(input, ref cursor, out capture));
				Assert.IsTrue(capture.IsValid);
				Assert.AreEqual(rule, capture.Rule);
				Assert.AreEqual((Cursor)pos, capture.Position);
				Assert.AreEqual(2, capture.Length);
				Assert.AreEqual(i.ToString("00"), capture.Value);
			}

			Assert.IsFalse(rule.TryCapture(input, ref cursor, out capture));
			Assert.IsFalse(capture.IsValid, "there is not a month with 32 days");
		}

		[TestMethod]
		public void CaptureIsoHours()
		{
			var rule = CharRule.Iso8601TimeHours;

			var input = @"00010203040506070809101112131415161718192021222324";

			Cursor cursor = new Cursor(0);
			Capture capture = default(Capture);
			for (int i = 0; i < 24; i++)
			{
				int pos = cursor;
				Assert.IsTrue(rule.TryCapture(input, ref cursor, out capture));
				Assert.IsTrue(capture.IsValid);
				Assert.AreEqual(rule, capture.Rule);
				Assert.AreEqual((Cursor)pos, capture.Position);
				Assert.AreEqual(2, capture.Length);
				Assert.AreEqual(i.ToString("00"), capture.Value);
			}

			Assert.IsFalse(rule.TryCapture(input, ref cursor, out capture));
			Assert.IsFalse(capture.IsValid, "hours of the day range from 00 (start of day midnight) to 24 (end of day midnight)");
		}

		[TestMethod]
		public void CaptureIsoMinutes()
		{
			var rule = CharRule.Iso8601TimeMinutes;

			var input = @"00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960";


			Cursor cursor = new Cursor(0);
			Capture capture = default(Capture);
			for (int i = 0; i < 60; i++)
			{
				int pos = cursor;
				Assert.IsTrue(rule.TryCapture(input, ref cursor, out capture));
				Assert.IsTrue(capture.IsValid);
				Assert.AreEqual(rule, capture.Rule);
				Assert.AreEqual((Cursor)pos, capture.Position);
				Assert.AreEqual(2, capture.Length);
				Assert.AreEqual(i.ToString("00"), capture.Value);
			}

			Assert.IsFalse(rule.TryCapture(input, ref cursor, out capture));
			Assert.IsFalse(capture.IsValid, "minutes of the day range from 00 (first minute of the hour) to 59 (last minute of the hour)");
		}

		[TestMethod]
		public void CaptureIsoSeconds()
		{
			var rule = CharRule.Iso8601TimeSecondsWithFraction;

			var input = @"00|01.1|02.1|03.3|04.4|05.1234567|06070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960";


			Cursor cursor = new Cursor(0);
			Capture capture = default(Capture);
			for (int i = 0; i < 60; i++)
			{
				int pos = cursor;
				Assert.IsTrue(rule.TryCapture(input, ref cursor, out capture));
				Assert.IsTrue(capture.IsValid);
				Assert.AreEqual(rule, capture.Rule);
				Assert.AreEqual((Cursor)pos, capture.Position);
				if (i < 6)
				{
					cursor++;
				}
				else
				{
					Assert.AreEqual(2, capture.Length);
					Assert.AreEqual(i.ToString("00"), capture.Value);
				}
			}

			Assert.IsFalse(rule.TryCapture(input, ref cursor, out capture));
			Assert.IsFalse(capture.IsValid, "minutes of the day range from 00 (first minute of the hour) to 59 (last minute of the hour)");
		}

		[TestMethod]
		public void CaptureIsoTimezone_Positive()
		{
			var rule = CharRule.Iso8601Timezone;

			var input = @"+00:00+01:01+02:02+03:03+04:04+05:05+06:06+07:07+08:08+09:09+10:10+11:11+12:12+13:13+14:14+15:15+16:16+17:17+18:18+19:19+20:20+21:21+22:22+23:23+24:00+25:25";


			Cursor cursor = new Cursor(0);
			Capture capture = default(Capture);
			for (int i = 0; i < 24; i++)
			{
				int pos = cursor;
				Assert.IsTrue(rule.TryCapture(input, ref cursor, out capture));
				Assert.IsTrue(capture.IsValid);
				Assert.AreEqual(rule, capture.Rule);
				Assert.AreEqual((Cursor)pos, capture.Position);
				Assert.AreEqual(6, capture.Length);
				Assert.AreEqual(String.Concat('+', i.ToString("00"), ':', i.ToString("00")), capture.Value);
			}

			Assert.IsTrue(rule.TryCapture(input, ref cursor, out capture));
			Assert.IsTrue(capture.IsValid);
			Assert.AreEqual(rule, capture.Rule);
			Assert.AreEqual(6, capture.Length);
			Assert.AreEqual("+24:00", capture.Value);

			Assert.IsFalse(rule.TryCapture(input, ref cursor, out capture));
			Assert.IsFalse(capture.IsValid, "timezones are from -24:00 to +24:00, minutes are optional");
		}

		[TestMethod]
		public void CaptureIsoTimezone_Negative()
		{
			var rule = CharRule.Iso8601Timezone;

			var input = @"-00:00-01:01-02:02-03:03-04:04-05:05-06:06-07:07-08:08-09:09-10:10-11:11-12:12-13:13-14:14-15:15-16:16-17:17-18:18-19:19-20:20-21:21-22:22-23:23-24:00-25:25";


			Cursor cursor = new Cursor(0);
			Capture capture = default(Capture);
			for (int i = 0; i < 24; i++)
			{
				int pos = cursor;
				Assert.IsTrue(rule.TryCapture(input, ref cursor, out capture));
				Assert.IsTrue(capture.IsValid);
				Assert.AreEqual(rule, capture.Rule);
				Assert.AreEqual((Cursor)pos, capture.Position);
				Assert.AreEqual(6, capture.Length);
				Assert.AreEqual(String.Concat('-', i.ToString("00"), ':', i.ToString("00")), capture.Value);
			}

			Assert.IsTrue(rule.TryCapture(input, ref cursor, out capture));
			Assert.IsTrue(capture.IsValid);
			Assert.AreEqual(rule, capture.Rule);
			Assert.AreEqual(6, capture.Length);
			Assert.AreEqual("-24:00", capture.Value);

			Assert.IsFalse(rule.TryCapture(input, ref cursor, out capture));
			Assert.IsFalse(capture.IsValid, "timezones are from -24:00 to +24:00, minutes are optional");
		}

	}
}
